/**
* @license 
* jQuery Tools Validator 1.2.5 - HTML5 is here. Now use it.
* 
* NO COPYRIGHTS OR LICENSES. DO WHAT YOU LIKE.
* 
* http://flowplayer.org/tools/form/validator/
* 
* Since: Mar 2010
* Date:    Wed Sep 22 06:02:10 2010 +0000 
*/
/*jslint evil: true */
(function ($) {

    $.tools = $.tools || { version: '1.2.5' };

    // globals
    var typeRe = /\[type=([a-z]+)\]/,
		numRe = /^-?[0-9]*(\.[0-9]+)?$/,
		dateInput = $.tools.dateinput,

    // http://net.tutsplus.com/tutorials/other/8-regular-expressions-you-should-know/
		emailRe = /^([a-z0-9_\.\-\+]+)@([\da-z\.\-]+)\.([a-z\.]{2,6})$/i,
		urlRe = /^(https?:\/\/)?[\da-z\.\-]+\.[a-z\.]{2,6}[#&+_\?\/\w \.\-=]*$/i,
		v;

    v = $.tools.validator = {

        conf: {
            grouped: false, 				// show all error messages at once inside the container 
            effect: 'default', 		// show/hide effect for error message. only 'default' is built-in
            errorClass: 'invalid', 	// input field class name in case of validation error		
            container: "body",
            // when to check for validity?
            inputEvent: null, 			// change, blur, keyup, null 
            errorInputEvent: 'keyup',  // change, blur, keyup, null
            formEvent: 'submit',       // submit, null

            lang: 'en', 					// default language for error messages 
            message: '<div/>',
            messageAttr: 'data-message', // name of the attribute for overridden error message
            messageClass: 'error', 	// error message element's class name
            offset: [0, 0],
            position: 'center right',
            singleError: false, 			// validate all inputs at once
            speed: 'normal',				// message's fade-in speed	
            zIndex: 10
        },


        /* The Error Messages */
        messages: {
            "*": { en: "Please correct this value" }
        },

        localize: function (lang, messages) {
            $.each(messages, function (key, msg) {
                v.messages[key] = v.messages[key] || {};
                v.messages[key][lang] = msg;
            });
        },

        localizeFn: function (key, messages) {
            v.messages[key] = v.messages[key] || {};
            $.extend(v.messages[key], messages);
        },

        /** 
        * Adds a new validator 
        */
        fn: function (matcher, msg, fn) {

            // no message supplied
            if ($.isFunction(msg)) {
                fn = msg;

                // message(s) on second argument
            } else {
                if (typeof msg == 'string') { msg = { en: msg }; }
                this.messages[matcher.key || matcher] = msg;
            }

            // check for "[type=xxx]" (not supported by jQuery)
            var test = typeRe.exec(matcher);
            if (test) { matcher = isType(test[1]); }

            // add validator to the arsenal
            fns.push([matcher, fn]);
        },

        /* Add new show/hide effect */
        addEffect: function (name, showFn, closeFn) {
            effects[name] = [showFn, closeFn];
        }

    };

    /* calculate error message position relative to the input */
    function getPosition(trigger, el, conf) {

        // get origin top/left position 
        var top = trigger.offset().top,
			 left = trigger.offset().left,
			 pos = conf.position.split(/,?\s+/),
			 y = pos[0],
			 x = pos[1];
        top -= el.outerHeight() - conf.offset[0];
        left += trigger.outerWidth() + conf.offset[1];


        // iPad position fix
        if (/iPad/i.test(navigator.userAgent)) {
            top -= $(window).scrollTop();
        }

        // adjust Y		
        var height = el.outerHeight() + trigger.outerHeight();
        if (y == 'center') { top += height / 2; }
        if (y == 'bottom') { top += height; }

        // adjust X
        var width = trigger.outerWidth();
        if (x == 'center') { left -= (width + el.outerWidth()) / 2; }
        if (x == 'left') { left -= width; }
        /*--> vinh.t.trieu */
        if ($(conf.container).scrollTop()) {
            top += $(conf.container).scrollTop();
        }
        /*<---end*/
        return { top: top, left: left };
    }



    // $.is("[type=xxx]") or $.filter("[type=xxx]") not working in jQuery 1.3.2 or 1.4.2
    function isType(type) {
        function fn() {
            return this.getAttribute("type") == type;
        }
        fn.key = "[type=" + type + "]";
        return fn;
    }


    var fns = [], effects = {

        'default': [

        // show errors function
			function (errs) {

			    var conf = this.getConf();

			    // loop errors
			    $.each(errs, function (i, err) {

			        // add error class	
			        var input = err.input;
			        input.addClass(conf.errorClass);

			        // get handle to the error container
			        var msg = input.data("msg.el");

			        // create it if not present
			        if (!msg) {
			            msg = $(conf.message).addClass(conf.messageClass).appendTo(conf.container);
			            msg.css("z-index", conf.zIndex);
			            input.data("msg.el", msg);
			        }

			        // clear the container 
			        msg.css({ visibility: 'hidden' }).find("p").remove();

			        // populate messages
			        $.each(err.messages, function (i, m) {
			            $("<p/>").html(m).appendTo(msg);
			        });

			        // make sure the width is not full body width so it can be positioned correctly
			        if (msg.outerWidth() == msg.parent().width()) {
			            msg.add(msg.find("p")).css({ display: 'inline' });
			        }

			        // insert into correct position (relative to the field)
			        var pos = getPosition(input, msg, conf);

			        msg.css({ visibility: 'visible', position: 'absolute', top: pos.top, left: pos.left })
						.fadeIn(conf.speed);
			    });


			    // hide errors function
			}, function (inputs) {

			    var conf = this.getConf();
			    inputs.removeClass(conf.errorClass).each(function () {
			        var msg = $(this).data("msg.el");
			        if (msg) { msg.css({ visibility: 'hidden' }); }
			    });
			}
        ]
    };


    /* sperial selectors */
    $.each("email,url,number".split(","), function (i, key) {
        $.expr[':'][key] = function (el) {
            return el.getAttribute("type") === key;
        };
    });


    /* 
    oninvalid() jQuery plugin. 
    Usage: $("input:eq(2)").oninvalid(function() { ... });
    */
    $.fn.oninvalid = function (fn) {
        return this[fn ? "bind" : "trigger"]("OI", fn);
    };


    /******* built-in HTML5 standard validators *********/

    v.fn(":email", "Please enter a valid email address", function (el, v) {
        return !v || emailRe.test(v);
    });

    v.fn(":url", "Please enter a valid URL", function (el, v) {
        return !v || urlRe.test(v);
    });

    v.fn(":number", "Please enter a numeric value.", function (el, v) {
        return numRe.test(v);
    });
    v.fn("[email]", "Please enter a valid email address", function (el, v) {
        return !v || emailRe.test(v);
    });

    v.fn("[url]", "Please enter a valid URL", function (el, v) {
        return !v || urlRe.test(v);
    });

    v.fn("[number]", "Please enter a numeric value.", function (el, v) {
        return numRe.test(v);
    });

    v.fn("[max]", "Please enter a value smaller than $1", function (el, v) {

        // skip empty values and dateinputs
        if (v === '' || dateInput && el.is(":date")) { return true; }

        var max = el.attr("max");
        return parseFloat(v) <= parseFloat(max) ? true : [max];
    });

    v.fn("[min]", "Please enter a value larger than $1", function (el, v) {

        // skip empty values and dateinputs
        if (v === '' || dateInput && el.is(":date")) { return true; }

        var min = el.attr("min");
        return parseFloat(v) >= parseFloat(min) ? true : [min];
    });

    v.fn("[required]", "Please complete this field.", function (el, v) {
        if (el.is(":checkbox")) { return el.is(":checked"); }
        return !!v;
    });

    v.fn("[pattern]", function (el) {
        var p = new RegExp("^" + el.attr("pattern") + "$");
        return p.test(el.val());
    });

    v.fn("[minlength]", function (input, value) {
        var min = input.attr("minlength");
        return value.length >= min ? true : {
            en: "Please provide at least " + min + " character" + (min > 1 ? "s" : "")
        };
    });

    v.fn("[data-equals]", "Value not equal with the $1 field", function (input) {
        var name = input.attr("data-equals"),
		 field = this.getInputs().filter("[id=" + name + "]");
        name = name.replace("txt", "");
        return input.val() == field.val() ? true : [name];
    });
    v.fn("[time]", "Please supply a valid time", function (input, value) {
        return /^\d\d:\d\d$/.test(value);
    });
    function Validator(inputs, form, conf) {

        // private variables
        var self = this,
			 fire = form.add(self);

        // make sure there are input fields available
        inputs = inputs.not(":button, :image, :reset, :submit");

        // utility function
        function pushMessage(to, matcher, returnValue) {

            // only one message allowed
            if (!conf.grouped && to.length) { return; }

            // the error message
            var msg;

            // substitutions are returned
            if (returnValue === false || $.isArray(returnValue)) {
                msg = v.messages[matcher.key || matcher] || v.messages["*"];
                msg = msg[conf.lang] || v.messages["*"].en;

                // substitution
                var matches = msg.match(/\$\d/g);

                if (matches && $.isArray(returnValue)) {
                    $.each(matches, function (i) {
                        msg = msg.replace(this, returnValue[i]);
                    });
                }

                // error message is returned directly
            } else {
                msg = returnValue[conf.lang] || returnValue;
            }

            to.push(msg);
        }


        // API methods  
        $.extend(self, {

            getConf: function () {
                return conf;
            },

            getForm: function () {
                return form;
            },

            getInputs: function () {
                return inputs;
            },

            reflow: function () {
                inputs.each(function () {
                    var input = $(this),
						 msg = input.data("msg.el");

                    if (msg) {
                        var pos = getPosition(input, msg, conf);
                        msg.css({ top: pos.top, left: pos.left });
                    }
                });
                return self;
            },

            /* @param e - for internal use only */
            invalidate: function (errs, e) {

                // errors are given manually: { fieldName1: 'message1', fieldName2: 'message2' }
                if (!e) {
                    var errors = [];
                    $.each(errs, function (key, val) {
                        var input = inputs.filter("[name='" + key + "']");
                        if (input.length) {

                            // trigger HTML5 ininvalid event
                            input.trigger("OI", [val]);

                            errors.push({ input: input, messages: [val] });
                        }
                    });

                    errs = errors;
                    e = $.Event();
                }

                // onFail callback
                e.type = "onFail";
                fire.trigger(e, [errs]);

                // call the effect
                if (!e.isDefaultPrevented()) {
                    effects[conf.effect][0].call(self, errs, e);
                }

                return self;
            },

            reset: function (els) {
                els = els || inputs;
                els.removeClass(conf.errorClass).each(function () {
                    var msg = $(this).data("msg.el");
                    if (msg) {
                        msg.remove();
                        $(this).data("msg.el", null);
                    }
                }).unbind(conf.errorInputEvent || '');
                return self;
            },

            destroy: function () {
                form.unbind(conf.formEvent + ".V").unbind("reset.V");
                inputs.unbind(conf.inputEvent + ".V").unbind("change.V");
                return self.reset();
            },


            //{{{  checkValidity() - flesh and bone of this tool

            /* @returns boolean */
            checkValidity: function (els, e) {

                els = els || inputs;
                els = els.not(":disabled");
                if (!els.length) { return true; }

                e = e || $.Event();

                // onBeforeValidate
                e.type = "onBeforeValidate";
                fire.trigger(e, [els]);
                if (e.isDefaultPrevented()) { return e.result; }

                // container for errors
                var errs = [];

                // loop trough the inputs
                els.not(":radio:not(:checked)").each(function () {

                    // field and it's error message container						
                    var msgs = [],
						 el = $(this).data("messages", msgs),
						 event = dateInput && el.is(":date") ? "onHide.v" : conf.errorInputEvent + ".v";

                    // cleanup previous validation event
                    el.unbind(event);


                    // loop all validator functions
                    $.each(fns, function () {
                        var fn = this, match = fn[0];

                        // match found
                        if (el.filter(match).length) {

                            // execute a validator function
                            var returnValue = fn[1].call(self, el, el.val());


                            // validation failed. multiple substitutions can be returned with an array
                            if (returnValue !== true) {

                                // onBeforeFail
                                e.type = "onBeforeFail";
                                fire.trigger(e, [el, match]);
                                if (e.isDefaultPrevented()) { return false; }

                                // overridden custom message
                                var msg = el.attr(conf.messageAttr);
                                if (msg) {
                                    msgs = [msg];
                                    return false;
                                } else {
                                    pushMessage(msgs, match, returnValue);
                                }
                            }
                        }
                    });

                    if (msgs.length) {

                        errs.push({ input: el, messages: msgs });

                        // trigger HTML5 ininvalid event
                        el.trigger("OI", [msgs]);

                        // begin validating upon error event type (such as keyup) 
                        if (conf.errorInputEvent) {
                            el.bind(event, function (e) {
                                self.checkValidity(el, e);
                            });
                        }
                    }

                    if (conf.singleError && errs.length) { return false; }

                });


                // validation done. now check that we have a proper effect at hand
                var eff = effects[conf.effect];
                if (!eff) { throw "Validator: cannot find effect \"" + conf.effect + "\""; }

                // errors found
                if (errs.length) {
                    self.invalidate(errs, e);
                    return false;

                    // no errors
                } else {

                    // call the effect
                    eff[1].call(self, els, e);

                    // onSuccess callback
                    e.type = "onSuccess";
                    fire.trigger(e, [els]);

                    els.unbind(conf.errorInputEvent + ".v");
                }

                return true;
            }
            //}}} 

        });

        // callbacks	
        $.each("onBeforeValidate,onBeforeFail,onFail,onSuccess".split(","), function (i, name) {

            // configuration
            if ($.isFunction(conf[name])) {
                $(self).bind(name, conf[name]);
            }

            // API methods				
            self[name] = function (fn) {
                if (fn) { $(self).bind(name, fn); }
                return self;
            };
        });


        // form validation
        if (conf.formEvent) {
            form.bind(conf.formEvent + ".V", function (e) {
                if (!self.checkValidity(null, e)) {
                    return e.preventDefault();
                }
            });
        }

        // form reset
        form.bind("reset.V", function () {
            self.reset();
        });

        // disable browser's default validation mechanism
        if (inputs[0] && inputs[0].validity) {
            inputs.each(function () {
                this.oninvalid = function () {
                    return false;
                };
            });
        }

        // Web Forms 2.0 compatibility
        if (form[0]) {
            form[0].checkValidity = self.checkValidity;
        }

        // input validation               
        if (conf.inputEvent) {
            inputs.bind(conf.inputEvent + ".V", function (e) {
                self.checkValidity($(this), e);
            });
        }

        // checkboxes, selects and radios are checked separately
        inputs.filter(":checkbox, select").filter("[required]").bind("change.V", function (e) {
            var el = $(this);
            if (this.checked || (el.is("select") && $(this).val())) {
                effects[conf.effect][1].call(self, el, e);
            }
        });

        var radios = inputs.filter(":radio").change(function (e) {
            self.checkValidity(radios, e);
        });

        // reposition tooltips when window is resized
        $(window).resize(function () {
            self.reflow();
        });
    }


    // jQuery plugin initialization
    $.fn.validator = function (conf) {

        var instance = this.data("validator");

        // destroy existing instance
        if (instance) {
            instance.destroy();
            this.removeData("validator");
        }

        // configuration
        conf = $.extend(true, {}, v.conf, conf);

        // selector is a form		
        if (this.is("form")) {
            return this.each(function () {
                var form = $(this);
                instance = new Validator(form.find(":input"), form, conf);
                form.data("validator", instance);
            });

        } else {
            instance = new Validator(this, this.eq(0).closest("form"), conf);
            return this.data("validator", instance);
        }

    };

})(jQuery);


